package de.stefanocke.roo.wicket.model;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.roo.classpath.customdata.tagkeys.FieldMetadataCustomDataKey;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.MemberFindingUtils;
import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadata;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;

import de.stefanocke.roo.wicket.AnnotationMetadataWithHashcode;
import de.stefanocke.roo.wicket.RooProperty;

/**
 * Metadata for a field to to be rendered in the web GUI.
 * Refers to some bean property in the domain model.
 * 
 * 
 * @author stoc
 * 
 */
public class BeanPropertyMetadata {
	private static final String CONTEXT = "context";
	private static final String EXCLUDE = "exclude";
	public static final JavaType ROOPROPERTY = new JavaType(RooProperty.class.getName());
	private FieldMetadata field;
	private JavaSymbolName propertyName;
	private JavaType type;
	private boolean collection;
	private List<AnnotationMetadata> additionalAnnotations = new ArrayList<AnnotationMetadata>();
	private List<AnnotationMetadata> effectiveAnnotations = null;
	private JavaType fieldConstantGovernor; //TODO: Remove this, as soon as BeanPropertiesMetadata is used directly

	public BeanPropertyMetadata() {
	}

	public BeanPropertyMetadata(FieldMetadata field, JavaSymbolName propertyName, JavaType type, boolean collection,
			JavaType fieldConstantGovernor) {
		super();
		this.field = field;
		this.propertyName = propertyName;
		this.type = type;
		this.collection = collection;
		this.fieldConstantGovernor = fieldConstantGovernor;
		updateEffectiveAnnotations();
	}

	public BeanPropertyMetadata(BeanPropertyMetadata other) {
		super();
		this.field = other.field;
		this.propertyName = other.propertyName;
		this.type = other.type;
		this.collection = other.collection;
		this.additionalAnnotations = new ArrayList<AnnotationMetadata>(other.additionalAnnotations);
		this.fieldConstantGovernor = other.fieldConstantGovernor;
		updateEffectiveAnnotations();
	}

	public void setField(FieldMetadata field) {
		this.field = field;
		updateEffectiveAnnotations();
	}

	public FieldMetadata getField() {
		return field;
	}

	public JavaSymbolName getPropertyName() {
		return propertyName;
	}

	public void setPropertyName(JavaSymbolName propertyName) {
		this.propertyName = propertyName;
	}

	public String getFieldConstantName() {
		return getPropertyName().getSymbolName().toUpperCase();
	}

	public String getFieldAnnotationsConstantName() {
		return getPropertyName().getSymbolName().toUpperCase() + "_ANNOTATIONS";
	}

	public void setCollection(boolean collection) {
		this.collection = collection;
	}

	public boolean isCollection() {
		return collection;
	}

	// the type of the property. If the property is collection valued, this is
	// the element type.
	public JavaType getType() {
		return type;
	}

	public void setType(JavaType type) {
		this.type = type;
	}

	public boolean hasFieldKey(FieldMetadataCustomDataKey key) {
		return field != null && field.getCustomData().keySet().contains(key);
	}

	public boolean isExcluded() {
		return getBooleanAttribute(EXCLUDE, false);
	}
	
	public boolean isContext(){
		return getBooleanAttribute(CONTEXT, false);
	}

	private boolean getBooleanAttribute(String attributeName, boolean defaultValue) {
		AnnotationMetadata fieldAnnotation = getRooPropertyAnnotation();
		if (fieldAnnotation == null) {
			return defaultValue;
		}		
		AnnotationAttributeValue<?> attrValue = fieldAnnotation.getAttribute(new JavaSymbolName(attributeName));
		if(attrValue==null){
			return defaultValue;
		}
		return Boolean.TRUE.equals(attrValue.getValue());
	}

	public AnnotationMetadata getRooPropertyAnnotation() {
		return getAnnotationOfType(ROOPROPERTY);
	}

	public boolean isBooleanType() {
		return "java.lang.Boolean".equals(getType().getFullyQualifiedTypeName());
	}

	public boolean isDateTimeType() {
		String fqn = getType().getFullyQualifiedTypeName();
		return "java.util.Date".equals(fqn) || "java.util.Calendar".equals(fqn);
	}

	public List<AnnotationMetadata> getAdditionalAnnotations() {
		return Collections.unmodifiableList(additionalAnnotations);
	}

	/**
	 * Gets the annotations on the field plus the additional annotations. The
	 * additional annotations have precedence over the ones on the field.
	 * 
	 * @return
	 */
	public List<AnnotationMetadata> getEffectiveAnnotations() {
		return effectiveAnnotations;
	}

	private void updateEffectiveAnnotations() {
		effectiveAnnotations = new ArrayList<AnnotationMetadata>(additionalAnnotations);
		Set<JavaType> annoTypes = new HashSet<JavaType>();
		for (AnnotationMetadata am : additionalAnnotations) {
			annoTypes.add(am.getAnnotationType());
		}
		if (field != null) {
			for (AnnotationMetadata am : field.getAnnotations()) {
				if (!annoTypes.contains(am.getAnnotationType())) {
					effectiveAnnotations.add(new AnnotationMetadataWithHashcode(am));
				}
			}
		}
	}

	public AnnotationMetadata getAnnotationOfType(Class<? extends Annotation> annotationType) {
		return MemberFindingUtils
				.getAnnotationOfType(getEffectiveAnnotations(), new JavaType(annotationType.getName()));
	}

	public AnnotationMetadata getAnnotationOfType(JavaType annotationType) {
		return MemberFindingUtils.getAnnotationOfType(getEffectiveAnnotations(), annotationType);
	}
	
	public JavaType getFieldConstantGovernor() {
		return fieldConstantGovernor;
	}
	
	public void setFieldConstantGovernor(JavaType fieldConstantGovernor) {
		this.fieldConstantGovernor = fieldConstantGovernor;
	}

	/**
	 * Adds annotations that come from some custom metadata and are related to
	 * the property
	 * 
	 * @param annotations
	 */
	public void addAnnotations(List<AnnotationMetadata> annotations) {
		for (AnnotationMetadata am : annotations) {
			additionalAnnotations.add(new AnnotationMetadataWithHashcode(am));
		}
		updateEffectiveAnnotations();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + (collection ? 1231 : 1237);
		result = prime * result + ((effectiveAnnotations == null) ? 0 : effectiveAnnotations.hashCode());
		result = prime * result + ((propertyName == null) ? 0 : propertyName.hashCode());
		result = prime * result + ((type == null) ? 0 : type.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		BeanPropertyMetadata other = (BeanPropertyMetadata) obj;
		if (collection != other.collection)
			return false;
		if (effectiveAnnotations == null) {
			if (other.effectiveAnnotations != null)
				return false;
		} else if (!effectiveAnnotations.equals(other.effectiveAnnotations))
			return false;
		if (propertyName == null) {
			if (other.propertyName != null)
				return false;
		} else if (!propertyName.equals(other.propertyName))
			return false;
		if (type == null) {
			if (other.type != null)
				return false;
		} else if (!type.equals(other.type))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return "BeanPropertyMetadata [propertyName=" + propertyName + ", type=" + type + ", collection=" + collection
				+ ", effectiveAnnotations=" + effectiveAnnotations + "]";
	}

}
